Skip to content

Add entity lifecycle status MCP tools (apps + components) - #23

Merged
bburda merged 6 commits into
mainfrom
feat/lifecycle-status
Aug 23, 2026
Merged

Add entity lifecycle status MCP tools (apps + components)#23
bburda merged 6 commits into
mainfrom
feat/lifecycle-status

Conversation

@bburda

@bburda bburda commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Add ros2_medkit_status_get and ros2_medkit_status_set MCP tools (with sovd_status_get / sovd_status_set back-compat aliases), wrapping the gateway 0.6.0 entity lifecycle API. status_get reads the readiness status; status_set triggers a transition (start / restart / force-restart / shutdown / force-shutdown). Restricted to apps and components; other entity types and unknown actions are rejected with a clear error.

Transitions are answered by a gateway-side LifecycleProvider plugin and no provider ships with the gateway, so a gateway without one answers 501 not-implemented while status_get still works. The README and the tool description say so rather than promising a 202.

Two error-path bugs surfaced while covering that and are fixed here:

  • A 3xx was treated as success. Its parsed body is None exactly like a body-less 202, so a proxy answering a destructive PUT with a redirect read as accepted. Only 2xx counts now.
  • A status the SDK documents (400/404/500) carrying a body that is not JSON - a proxy error page - raised out of the generated call and lost its HTTP status, reporting Failed to parse response with no status code. The status is preserved now.

Issue


Type

  • Bug fix
  • New feature
  • Breaking change
  • Documentation only

Testing

In a worktree branched from origin/main (post-0.6.0-migration):

  • run_tests.py - 216 passed
  • ruff check src/ tests/ - clean
  • ruff format --check src/ tests/ - clean
  • mypy src/ - clean

Verified against a running gateway container rather than mocks: status_get returns ready for an app and for the host component, and all five transitions return 501 [not-implemented] Lifecycle control not available for this entity with no lifecycle provider registered.

Covered by tests: the 501 path for both entity types across all five actions, every entity_type x action route, redirect and non-JSON-error-body handling, and the registered MCP handlers (tool registry, schema enums, dispatch, sovd_* alias), which no test reached before.


Checklist

  • Breaking changes are clearly described (none - additive feature)
  • Linting passes (poetry run ruff check src/ tests/)
  • Formatting passes (poetry run ruff format --check src/ tests/)
  • Type checking passes (poetry run mypy src/)
  • Tests pass (poetry run python run_tests.py)
  • Docs were updated if behavior or public API changed (README lifecycle tools section)

Expose ros2_medkit_status_get/set for apps and components (start,
restart, force-restart, shutdown, force-shutdown), with sovd_*
back-compat aliases. Wraps the gateway 0.6.0 lifecycle API; rejects
other entity types and unknown actions.
Copilot AI lite review requested due to automatic review settings June 25, 2026 08:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class MCP tool support for the ros2_medkit gateway (v0.6.0) entity lifecycle status API, allowing clients to query readiness state and trigger lifecycle transitions for apps and components (with sovd_* aliases preserved for backward compatibility).

Changes:

  • Added lifecycle argument models (LifecycleEntityType, LifecycleAction, StatusGetArgs, StatusSetArgs) to validate entity-type and action inputs.
  • Added two new MCP tools (ros2_medkit_status_get, ros2_medkit_status_set) plus sovd_status_get / sovd_status_set aliases, wiring dispatch through to the client.
  • Implemented lifecycle API support in SovdClient (get_status, set_status) and added corresponding unit tests and README documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/ros2_medkit_mcp/client.py Adds lifecycle endpoint routing, validation, and get_status/set_status client methods backed by generated lifecycle API functions.
src/ros2_medkit_mcp/models.py Introduces Pydantic argument models/enums for lifecycle status tools with strict validation (apps/components + allowed actions).
src/ros2_medkit_mcp/mcp_app.py Registers the new MCP tools, adds aliases, and dispatches tool calls to the new client lifecycle methods.
tests/test_new_tools.py Adds integration-style client tests for lifecycle GET/PUT behavior and input rejection cases.
tests/test_mcp_app.py Adds unit tests for tool alias resolution and Pydantic validation of lifecycle argument models.
README.md Documents the new lifecycle tools and their arguments/return behavior.

@bburda bburda self-assigned this Aug 20, 2026
Comment thread README.md Outdated
A transition PUT is answered by a gateway-side LifecycleProvider plugin and
no provider ships with the gateway, so on a stock gateway every status_set
call comes back as 501 not-implemented while status_get works. Say so in the
README and in the tool description, and describe what the tool actually
returns on success: an empty JSON object, not a raw 202.

Cover the paths that carried the claim: the missing-provider 501 for all five
actions, both entity types against every action so a wrong _ENTITY_FUNC_MAP
entry shows as a request to the wrong URL, and the registered MCP handlers -
tool registry, schema enums, dispatch and the sovd_* alias - which no test
reached before.
Copilot AI review requested due to automatic review settings August 22, 2026 20:58
models.py switched to StrEnum for FaultStatus; the lifecycle enums are
declared the same way so the import stays a single name.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

README.md:390

  • The lifecycle API is introduced specifically with the gateway 0.6.0 contract, so older gateways that do not expose /{entity_type}/{entity_id}/status will return an error for reads as well. Please qualify this statement (for example, “Reading the status works on gateways that expose the 0.6.0 lifecycle API”) so users do not assume compatibility with every gateway version.
Reading the status works on any gateway. Triggering a transition requires the gateway
to have a `LifecycleProvider` plugin registered for the entity; there is no built-in

README.md:392

  • The MCP tool does not return the documented 501 prefix here: _call_void stores the status in SovdClientError.status_code, but its message for this error envelope is only [not-implemented] ..., and call_tool does not append status_code. This also conflicts with the exact output documented below on line 411; please describe the actual tool error (or change the formatter if the HTTP status must be exposed).
`501 [not-implemented] Lifecycle control not available for this entity`.

src/ros2_medkit_mcp/mcp_app.py:2643

  • This tool description also presents status_get as working on a stock gateway without stating the required gateway version. Since this client is pinned to and the feature is defined for gateway 0.6.0, older gateways can reject the GET endpoint; qualify the claim here so the tool metadata does not overstate compatibility.
                    " LifecycleProvider plugin for the entity; there is no"
                    " built-in provider, so a stock gateway answers every"
                    " transition with 'not-implemented' while status_get still"
                    " works. On success the gateway returns a body-less 202 and"

Copilot AI review requested due to automatic review settings August 22, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

README.md:392

  • SovdClientError keeps the HTTP status in status_code, but call_tool formats only str(e), so the tool output contains [not-implemented] Lifecycle control not available for this entity without the 501 prefix. This example disagrees with the actual response (and with the wording below); please update the documented error text or include status codes in the formatter.
`501 [not-implemented] Lifecycle control not available for this entity`.

src/ros2_medkit_mcp/mcp_app.py:3265

  • At this branch, an unsupported entity_type or action raises ValidationError, but the broad handler below converts it into Internal error: .... That mislabels invalid user input as a server failure and does not provide the clear rejection promised for lifecycle arguments. Handle validation errors as input errors (or validate these arguments before dispatch) so the response is not labeled internal.
                status_get_args = StatusGetArgs(**arguments)

A status outside 2xx is a failure. Only 4xx and 5xx were rejected, so a 3xx -
whose parsed body is None exactly like a body-less 202 - came back as an empty
success object. No endpoint documents a 3xx and redirects are not followed, but
a proxy in front of the gateway answering a destructive PUT with a 302 must not
read as accepted.

The docs claimed more than the gateway guarantees. A transition is not
universally 501 on a provider-less gateway: entity validation answers 400 and
404 first, and an aggregating gateway forwards a remote entity to the peer that
owns it, so a peer with a provider answers normally. Reading the status is not
unconditional either. Both claims now name the case they hold for.

Tests assert what the docs promise rather than a substring of it: the exact
empty object on success, the full error string the README quotes, every clause
of the tool description, the 501 for both entity types, and exactly one
outbound request with an empty body for a destructive transition.
Copilot AI review requested due to automatic review settings August 23, 2026 10:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

The generated parsers build their error model inside the API function, so a
status the SDK documents (400, 404, 500) carrying a body that is not JSON -
a proxy error page rather than the gateway's own envelope - raises out of the
call before _call_void sees the response. The status was lost with it, and the
caller got "Failed to parse response" with status_code None: a rejected
transition became indistinguishable from a client bug.

An httpx response hook records the status per task, so the handler can report
the real status and say plainly that the body was unparseable. A ContextVar
rather than an attribute because concurrent tool calls share the client.

The same loss in _call is upstream: MedkitClient.call never sees the response
and hardcodes status 0.
Copilot AI review requested due to automatic review settings August 23, 2026 12:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

src/ros2_medkit_mcp/mcp_app.py:3272

  • This validation raises ValidationError before SovdClient.set_status can produce its explicit lifecycle message, but the only matching handler below is the broad except Exception, so an invalid action is returned to MCP as Internal error: ... rather than as a client-input error. Catch lifecycle argument validation separately and format it as an invalid-arguments response so the rejection is not misdiagnosed as a server failure.
                status_set_args = StatusSetArgs(**arguments)

README.md:390

  • The phrase Triggering a transition does: is grammatically incomplete; use Triggering a transition does this: so the lifecycle documentation is clear.
for managed lifecycle nodes, from their reported state. Triggering a transition does: the

src/ros2_medkit_mcp/client.py:1522

  • This GET still goes through _call, which converts the generated client's None for an undocumented response status into {} (_to_dict(None)). As a result, a gateway/proxy failure such as HTTP 503 can be reported as a successful empty status response instead of an error, so callers cannot distinguish readiness data from an outage. Use a status-aware detailed call here (and require the status payload) before returning.
        return await self._call(fn, **{_entity_id_kwarg(entity_type): entity_id})

The transition failure was written as one quoted blob, which reads as though
the tool emits the 501. It does not: the gateway answers HTTP 501 and the tool
reports the error envelope without the status.
Copilot AI review requested due to automatic review settings August 23, 2026 12:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/ros2_medkit_mcp/client.py:569

  • The hook is installed after self._medkit is published at line 561. A concurrent first request can therefore take the fast path at line 557 while __aenter__() is still suspended, observe an uninitialized client (or issue a request before this hook exists), and fail to record the response status. Build and fully initialize a local MedkitClient, install the hook, then assign it to self._medkit so initialization is atomic to other callers.
                hooks = self._medkit.http.get_async_httpx_client().event_hooks
                hooks.setdefault("response", []).append(_record_response_status)

@bburda
bburda merged commit 2f265b4 into main Aug 23, 2026
4 checks passed
@bburda
bburda deleted the feat/lifecycle-status branch August 23, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add entity lifecycle status MCP tools (apps + components)

3 participants